﻿using CommonModel.Util;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace GXRTBTC.PublicClassLibrary.Util
{
    public class SNMPUtil
    {
        /// <summary>
        /// snmp报文拼接
        /// 这里只生成get、set报文
        /// </summary>
        /// <param name="oids">oid集合</param>
        /// <param name="tagType">默认为get 0xA0，可选为set 0xA3 set时，oids的value必须有int值 不考虑其他的TagType</param>
        /// <param name="ver">snmp版本，默认V2c版本</param>
        /// <returns>命令hex</returns>
        public static string GetSnmpCmd(Dictionary<string, string> oids, TagType tagType = TagType.GET, Version ver = Version.V2c)
        {
            //整个报文是一个SEQUENCE，编码为0x30
            string snmpSeq = GetEnumDes(TagType.SEQ);
            string verStr = GetEnumDes(ver);
            //oid标识域编码，编码为0x06
            string oidCode = GetEnumDes(TagType.OBJID);
            //下发默认类型为null，编码为0x05
            string tpOid = tagType == TagType.GET ? GetEnumDes(TagType.NULL) : GetEnumDes(TagType.INT);
            //参数类型(OCTSTR)
            string octStr = GetEnumDes(TagType.OCTSTR);
            //get-request set-request
            string tpStr = GetEnumDes(tagType);
            //int
            string intType = GetEnumDes(TagType.INT);

            string bodyHex = "";
            foreach (string oid in oids.Keys)
            {
                string oidHex = OidToHex(oid);
                string keyValHex;
                //get
                if (tagType == TagType.GET)
                {
                    //值为null，长度是0
                    keyValHex = oidHex + tpOid + "00";
                }
                //set
                else
                {
                    //此处默认值只能为int
                    string val = oids[oid];
                    if (!TransForm.IsInt(val))
                    {
                        val = "0";
                    }
                    string valHex = ConvertHex(val);

                    //根据值，动态长度
                    keyValHex = oidHex + tpOid + GetLenHex(valHex.Length / 2) + valHex;
                }
                
                //oid拼接，oidCode+长度+值
                string tmpHex = oidCode + GetLenHex(oidHex.Length / 2) + keyValHex;
                //单个oid报文拼接，报文类型snmpSeq+长度+值
                bodyHex += snmpSeq + GetLenHex(tmpHex.Length / 2) + tmpHex;
            }
            //所有oid报文拼接，报文类型snmpSeq+长度+所有oid拼接成的值
            bodyHex = snmpSeq + GetLenHex(bodyHex.Length / 2) + bodyHex;

            //request-id
            string reqId = intType + GetLenHex(4) + ConvertHex(new Random().Next(100000000, 999999999).ToString());
            //error-status
            string errSt = intType + GetLenHex(1) + "00";
            //error-index
            string errIdx = intType + GetLenHex(1) + "00";

            //oid报文前，拼接上request-id + error-status + error-index
            bodyHex = reqId + errSt + errIdx + bodyHex;
            bodyHex = tpStr + GetLenHex(bodyHex.Length / 2) + bodyHex;

            //拼接community 7075626C6963为群体名public的assic码值
            bodyHex = octStr + GetLenHex(6) + "7075626C6963" + bodyHex;
            //拼接版本信息
            bodyHex = intType + GetLenHex(1) + verStr + bodyHex;
            //拼接头部
            bodyHex = snmpSeq + GetLenHex(bodyHex.Length / 2) + bodyHex;

            return bodyHex;
        }

        /// <summary>
        /// 根据snmp报文，解析出值
        /// 这里只解析get报文
        /// </summary>
        /// <param name="snmpHex"></param>
        /// <returns>返回Dictionary字典结果</returns>
        public static Dictionary<string, string> AnalySnmp(string snmpHex)
        {
            //结果集
            Dictionary<string, string> rs = new Dictionary<string, string>();

            //idx默认跳过第一字节
            int idx = 1, len, lenNum = 1;
            //字符串按照长度2截取，即字节，十六进制更直观
            List<string> array = Regex.Matches(snmpHex.ToUpper(), @"\w{2}").Cast<Match>().Select(m => m.Groups[0].Value).ToList();

            //第二字节，为总长度
            len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
            //根据总长度，作一次判断 总长度减去当前idx 减去长度所占位数
            if ((array.Count - idx - lenNum) == len)
            {
                //跳过长度所占字节
                idx += lenNum;

                //版本信息  编码1 长度1 值1 一共3 直接跳过
                idx += 3;

                //团体名处理
                //跳过团体编码1
                idx++;
                len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                //跳过团体长度所占字节
                idx += lenNum;
                //跳过团体所占字节
                idx += len;

                //跳过报文类型一个字节，这里应该时A2
                idx++;

                //报文类型后，又跟了一个长度
                len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                //跳过报文类型后长度所占字节
                idx += lenNum;

                //request-id
                //跳过request-id类型所占字节1
                idx++;
                //request-id长度
                len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                //跳过request-id长度所占字节
                idx += lenNum;
                //跳过request-id所占字节
                idx += len;

                //error-status 暂不处理，直接跳过3
                idx += 3;

                //error-index 暂不处理，直接跳过3
                idx += 3;

                //跳过值类型（30）1
                idx++;
                //值类型长度
                len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                //跳过值类型长度所占字节
                idx += lenNum;

                //从这里进入数据解析
                bool loop = true;
                while (loop)
                {
                    //跳过值类型（30）1
                    idx++;

                    //值类型长度
                    len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                    //跳过值类型长度所占字节
                    idx += lenNum;

                    //跳过oid类型1
                    idx++;
                    //oid长度
                    len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                    //跳过oid长度所占字节
                    idx += lenNum;

                    //oid获取
                    string oid = HexToOid(string.Join("", array.GetRange(idx, len)));
                    //跳过oid长度
                    idx += len;

                    //值类型
                    string type = array[idx];
                    idx++;
                    //值长度
                    if (idx + 3 <= array.Count)
                    {
                        len = GetLen(string.Join("", array.GetRange(idx, 3)), ref lenNum);
                    }
                    else if (idx + 2 <= array.Count)
                    {
                        len = GetLen(string.Join("", array.GetRange(idx, 2)), ref lenNum);
                    }
                    else
                    {
                        len = GetLen(string.Join("", array.GetRange(idx, 1)), ref lenNum);
                    }
                    //跳过值长度所占字节
                    idx += lenNum;

                    //值hex
                    string valHex = string.Join("", array.GetRange(idx, len));
                    idx += len;

                    //值
                    string val = "";

                    //int类型
                    if (type.Equals("02"))
                    { 
                        val = Convert.ToInt32(valHex, 16).ToString();
                    }

                    //放入结果集
                    rs.TryAdd(oid, val);

                    if (idx >= array.Count)
                    {
                        loop = false;
                    }
                }
            }

            return rs;
        }

        /// <summary>
        /// 长度域hex生成
        /// 长度域用于指示后续的值域value的字节数。但是这个长度域自身多长怎么确定呢？SNMP使用的是变长表示法，这有点类似与UTF8的编码方式。具体表示方法如下：
        ///     （1）如果值域的长度在0到127字节之间，那么就是一个字节来表示，即第一个最高位为0的时候，其值就代表了值域的长度。
        ///     （2）如果值域的长度在127字节以上，那么第一个字节的第一个bit位(为1)就用于指示值域的长度在127字节以上，后7个bit位(实际值)以及后续用于表示值域长度的字节数。
        ///     例如：
        /// 值域长度(十进制)	编码表示(十六进制)	解释
        /// 16	    0x10	        在0到127之间，直接表示
        /// 160	    0x81 0xA0	    0x81的二进制为1000 0001第一位表示其超过127，低7位表示后续还有一个字节来表示值域字节数。0xA0表示值域的长度为160个字节。
        /// 1500	0x82 0x05 0xDC	第一个字节0x82表示后续还有两个字节表示值域长度，0x05DC的十进制值为1500
        /// 因为绝大部分情况下，值域的长度都在【0,127】区间内，所有这种表示方法最节约。
        /// </summary>
        /// <param name="len"></param>
        /// <returns>hex</returns>
        public static string GetLenHex(int len)
        {
            string lenHex;
            if (len > 0 && len <= 127)
            {
                //在0到127字节之间
                lenHex = ConvertHex(len.ToString());
            }
            else
            {
                string tmpHex = ConvertHex(len.ToString());
                //长度二进制
                string bitStr = Convert.ToString(tmpHex.Length / 2, 2);
                //补足7的倍数 第一个bit位(固定为1)
                bitStr = "1" + bitStr.PadLeft(7, '0');

                //最终hex
                lenHex = ConvertHex(Convert.ToInt32(bitStr, 2).ToString()) + tmpHex;
            }
            return lenHex;
        }


        /// <summary>
        /// 根据长度域hex，返回长度
        /// </summary>
        /// <param name="lenHex">建议不要只传入参数1字节，要三字节，字符串长足至少为六</param>
        /// <param name="n">长度字段所占字节数，获取长度时，返回更新字节数，方便后续数据处理</param>
        /// <returns></returns>
        public static int GetLen(string lenHex, ref int n)
        {
            int len = 0;
            //字符串按照长度2截取
            List<string> array = Regex.Matches(lenHex, @"\w{2}").Cast<Match>().Select(m => m.Groups[0].Value).ToList();

            //取第一个字节（hex）
            int l = Convert.ToInt32(array[0], 16);
            //转换成二进制串
            string bitStr = Convert.ToString(l, 2);
            if (bitStr.Length < 8)
            {
                len = l;
                n = 1;
            }
            else
            {
                //只保留七位，第一位变成0
                bitStr = "0" + bitStr.Substring(1);
                //获取后面还有多少个字节表示长度
                l = Convert.ToInt32(bitStr, 2);

                string hex = "";
                for (int i = 0; i < l && array.Count > (i + 1); i++)
                {
                    hex += array[i + 1];
                }
                len = Convert.ToInt32(hex, 16);
                n = l + 1;
            }

            return len;
        }

        /// <summary>
        /// SNMP服务器维护的所有管理信息库(MIB)对象采用ObjectID表示,如,1.3.6.1.2.1.1.1表示MIB库中的设备描述SysDesc变量,其编码规则如下:标识域编码为0x06，长度域根据情况而定，值域的编码比较复杂，如下所示。
        /// 1．首两个ID被合并为一个字节X * 40+Y。
        /// 例如：1.3合并为1x40+3 =?43?=?0x2B
        /// 2．后续的ID，如果在区间[1,127]，直接编码表示，如果大于127，那么按照下面(3)所述方法编码。
        /// 3．如果ID大于127，那么使用多个字节来表示。
        /// a.这多个字节中除最后一个字节外，前面的字节最高位为1
        /// b.这多个字节的最后一个字节的最高位为0
        /// c.这里每个字节剩下的7个比特位用来表示实际的数值
        /// 例如201566这个数，用十六进制表示是0x03 13 5e，那么用二进制表示是000 1100?010 0110?101 1110。
        /// 注意上面是以7个比特位为单位进行分划的，现在我们来填充最高位。
        /// 将前面的最高位填1，最后一个最高位填0即可得到
        /// 1000 1100?1010 0110?0101 1110
        /// 用十六进制表示为0x8c a6 5e
        /// </summary>
        /// <param name="oid"></param>
        /// <returns>oid转码后的hex字符串</returns>
        public static string OidToHex(string oid)
        {
            string oidHex = "";
            string[] oidArr = oid.Split('.');
            int start = int.Parse(oidArr[0]);
            for (int i = 1; i < oidArr.Length; i++)
            {
                int id = int.Parse(oidArr[i]);

                //首两个ID被合并为一个字节X * 40+Y
                if (i == 1)
                {
                    int sum = start * 40 + id;
                    oidHex += ConvertHex(sum.ToString());
                }
                //判断是否在1~127之间，包含1和127
                else if (id >= 1 && id <= 127)
                {
                    oidHex += ConvertHex(oidArr[i]);
                }
                //如果ID大于127，那么使用多个字节来表示
                else
                {
                    //转换成二进制串
                    string bitStr = Convert.ToString(id, 2);
                    //补足7的倍数
                    bitStr = bitStr.PadLeft(7 * ((bitStr.Length / 7) + (bitStr.Length % 7 == 0 ? 0 : 1)), '0');
                    //字符串按照长度7截取
                    List<string> array = Regex.Matches(bitStr, @"\w{7}").Cast<Match>().Select(m => m.Groups[0].Value).ToList();

                    //将前面的最高位填1
                    for (int j = 0; j < array.Count - 1; j++)
                    {
                        array[j] = "1" + array[j];
                    }
                    //最后一个最高位填0
                    array[array.Count - 1] = "0" + array[array.Count - 1];

                    //循环写入
                    foreach (string btArr in array)
                    {
                        //转成int 再转成hex
                        oidHex += ConvertHex(Convert.ToInt32(btArr, 2).ToString());
                    }
                }                
            }

            //尾部补00
            return oidHex + "00";
        }

        /// <summary>
        /// 根据hex，反向解析出oid
        /// </summary>
        /// <param name="oidHex"></param>
        /// <returns></returns>
        public static string HexToOid(string oidHex)
        {
            List<string> oids = new List<string>();

            //字符串按照长度2截取
            List<string> array = Regex.Matches(oidHex, @"\w{2}").Cast<Match>().Select(m => m.Groups[0].Value).ToList();
            int idx = 0;
            while (idx < array.Count - 1)
            {
                int id = Convert.ToInt32(array[idx], 16);

                //首两个ID被合并为一个字节X * 40+Y 反向解出
                if (idx == 0)
                {
                    int sec = id % 40;

                    oids.Add(((id - sec) / 40).ToString());
                    oids.Add(sec.ToString());
                }
                //判断是否在1~127之间，包含1和127
                else if (id >= 1 && id <= 127)
                {
                    oids.Add(id.ToString());
                }
                //如果ID大于127，那么使用多个字节来表示
                else
                {
                    //转换成二进制串
                    string bitStr = Convert.ToString(id, 2);
                    string tmpBit = bitStr.Substring(1);

                    bool isNext = true;
                    while (isNext)
                    {
                        idx++;
                        id = Convert.ToInt32(array[idx], 16);
                        bitStr = Convert.ToString(id, 2);
                        if (bitStr.Length < 8)
                        {
                            //退出循环，补足7位
                            isNext = false;
                            bitStr = bitStr.PadLeft(7, '0');
                        }
                        else
                        {
                            //只保留七位
                            bitStr = bitStr.Substring(1);
                        }
                        tmpBit += bitStr;
                    }

                    oids.Add(Convert.ToInt32(tmpBit, 2).ToString());
                }
                idx++;
            }

            return string.Join(".", oids.ToArray());
        }

        /// <summary>
        /// 转换数字成单个16进制字符，要求输入值小于16 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string GetHexChar(string value)
        {
            string sReturn = string.Empty;
            switch (value)
            {
                case "10":
                    sReturn = "A";
                    break;
                case "11":
                    sReturn = "B";
                    break;
                case "12":
                    sReturn = "C";
                    break;
                case "13":
                    sReturn = "D";
                    break;
                case "14":
                    sReturn = "E";
                    break;
                case "15":
                    sReturn = "F";
                    break;
                default:
                    sReturn = value;
                    break;
            }
            return sReturn;
        }

        /// <summary>
        /// 返回进制字符串
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string ConvertHex(string value)
        {
            string sReturn = string.Empty;
            try
            {

                while (ulong.Parse(value) >= 16)
                {
                    ulong v = ulong.Parse(value);
                    sReturn = GetHexChar((v % 16).ToString()) + sReturn;
                    value = Math.Floor(Convert.ToDouble(v / 16)).ToString();
                }
                sReturn = GetHexChar(value) + sReturn;
            }
            catch
            {
                sReturn = "###Valid Value!###";
            }
            return (sReturn.Length % 2 == 1) ? "0" + sReturn : sReturn;
        }

        /**
         * ASN.1了解
         * ASN.1标准定义，数据类型分为简单数据类型和构造数据类型
         * 简单数据类型：
         * boolean 布尔值
         * null 空
         * integer 整型
         * real 实数
         * octerstring 八进制字符串
         * object identifier 对象标识
         * ipaddressIP 地址
         * time ticks 时刻值
         * 构造数据类型
         * sequence 序列
         * sequence of set
         * set ofchoice等
         */

        /**
         * 编码规则BER
         * 由三个域组成：标识域（tag）+长度域（length）+值域（value）
         * 长度域用于指示后续的值域value的字节数。但是这个长度域自身多长怎么确定呢？SNMP使用的是变长表示法，这有点类似与UTF8的编码方式。具体表示方法如下：
            （1）如果值域的长度在0到127字节之间，那么就是一个字节来表示，即第一个最高位为0的时候，其值就代表了值域的长度。
            （2）如果值域的长度在127字节以上，那么第一个字节的第一个bit位(为1)就用于指示值域的长度在127字节以上，后7个bit位(实际值)以及后续用于表示值域长度的字节数。
         *  16	    0x10	        在0到127之间，直接表示
            160	    0x81 0xA0	    0x81的二进制为1000 0001第一位表示其超过127，低7位表示后续还有一个字节来表示值域字节数。0xA0表示值域的长度为160个字节。
            1500	0x82 0x05 0xDC	第一个字节0x82表示后续还有两个字节表示值域长度，0x05DC的十进制值为1500
         */

        /// <summary>
        /// 标识域（Tag）
        /// </summary>
        public enum TagType
        {
            [Description("0x01")]
            BOOL,
            [Description("0x02")]
            INT,
            [Description("0x04")]
            OCTSTR,
            [Description("0x05")]
            NULL,
            [Description("0x06")]
            OBJID,
            [Description("0x0A")]
            ENUM,
            [Description("0x30")]
            SEQ,
            [Description("0x31")]
            SETOF,
            [Description("0x40")]
            IPADDR,
            [Description("0x41")]
            COUNTER,
            [Description("0x42")]
            GAUGE,
            [Description("0x43")]
            TIMETICKS,
            [Description("0x44")]
            OPAQUE,
            [Description("0xA0")]
            GET,
            [Description("0xA1")]
            GETNEXT,
            [Description("0xA2")]
            GETResp,
            [Description("0xA3")]
            SET,
            [Description("0xA4")]
            TRAP,
            [Description("0xA5")]
            GETBULK,    //SNMPv2增加
            [Description("0xA4")]
            INFORM      //SNMPv2增加
        }

        ///// <summary>
        ///// PDU类型编码 TagType已包含
        ///// </summary>
        //public enum PDUCode
        //{
        //    [Description("0xA0")]
        //    GET_REQ,    //get-request
        //    [Description("0xA1")]
        //    GET_NEXT_REQ,   //get-next-request
        //    [Description("0xA2")]
        //    GET_RESP,   //get-response
        //    [Description("0xA3")]
        //    SET_REQ,    //set-request
        //    [Description("0xA4")]
        //    TRAP,       //trap(notification)
        //    [Description("0xA5")]
        //    GET_BULK,   //GetBulk(SNMPv2增加)
        //    [Description("0xA6")]
        //    INFORM      //Inform(SNMPv2增加)
        //}

        /// <summary>
        /// SNMP版本
        /// </summary>
        public enum Version
        {
            [Description("0x00")]
            V1,
            [Description("0x01")]
            V2c,
            [Description("0x02")]
            V3
        }

        /// <summary>
        /// 错误状态
        /// </summary>
        public enum ErrType
        {
            noError = 0,    //一切正常
            tooBig = 1,     //代理无法将回答装入到一个SNMP报文之中
            noSuchName = 2, //操作指明了一个不存在的变量
            badValue = 3,   //一个set操作指明了一个无效值或无效语法
            readOnly = 4,   //管理进程试图修改一个只读变量
            genErr = 5      //某些其他的差错
        }

        /// <summary>
        /// trap类型
        /// </summary>
        public enum TrapType
        {
            coldStart = 0,      //代理进行了初始化
            warmStart = 1,      //代理进行了重新初始化
            linkDown = 2,       //一个接口从工作状态变为故障状态
            linkUp = 3,         //一个接口从故障状态变为工作状态
            authenticationFailure = 4,   //从SNMP管理进程接收到具有一个无效共同体的报文
            egpNeighborLoss = 5,    //一个EGP相邻路由器变为故障状态
            enterpriseSpecific = 6  //代理自定义的事件，需要用后面的“特定代码”来指明
        }

        /// <summary>
        /// 根据枚举值，获取描述
        /// </summary>
        /// <param name="enumValue"></param>
        /// <returns></returns>
        public static string GetEnumDes(Enum enumValue)
        {
            string value = enumValue.ToString();
            FieldInfo field = enumValue.GetType().GetField(value);
            object[] objs = field.GetCustomAttributes(typeof(DescriptionAttribute), false);    //获取描述属性
            if (objs == null || objs.Length == 0)    //当描述属性没有时，直接返回名称
                return value;
            DescriptionAttribute descriptionAttribute = (DescriptionAttribute)objs[0];

            string desc = descriptionAttribute.Description;
            if (desc.ToUpper().StartsWith("0X"))
            {
                desc = desc.Replace("0x", "").Replace("0X", "");
            }
            return desc;
        }
    }
}
